As someone who tends to be fairly paranoid when it comes to online security, I like the idea of using a hardware-based authentication device to store keys safely for things like code signing and SSH access.
Unfortunately, this turned out to be a bit painful to get working with WSL. After struggling for days on end to follow any tutorial with success, eventually giving up for a few months and going back and forth between a dual-boot of OpenSUSE, I finally decided to take a crack at it again.
With existing tutorials being seemingly useless, I had to figure out a method that didn’t involve forwarding the gpg/ssh-agent from Windows to WSL using a .exe from an unmaintained project on GitHub. Here’s what I came up with:
Before we get started, some requirements:
- YubiKey 5 * Series or later
- Windows 11
- WSL2 Version >= 0.67.6 running Ubuntu 20.04 (other distro/version may also work, I haven’t tested)
Getting USB passthrough set up
USB passthrough works via usbipd-win which allows for sharing locally connected USB devices to other machines, including Hyper-V guests and WSL2.
First, we must install the windows package, I used the Windows Package Manager.
winget install usbipd
But you can also find an installer for the latest release here.
Onto the WSL side, running uname -a
from within WSL should report a kernel version of 5.10.60.1 or later. Now let’s install the user space tools for USB/IP on Linux and a database of USB hardware identifiers.
$ sudo apt install linux-tools-virtual hwdata
$ sudo update-alternatives --install /usr/local/bin/usbip usbip `ls /usr/lib/linux-tools/*/usbip | tail -n1` 20
udev
It’s recommended to configure udev rules to allow non-root users to access the device. Yubico provides some rules for their cards here that you can copy to your /etc/udev/rules.d folder.
$ sudo wget https://github.com/Yubico/libfido2/blob/main/udev/70-u2f.rules > /etc/udev/rules.d/70-u2f.rules
Once udev rules are in place, we need to restart WSL. From a PowerShell do
wsl --shutdown
Checking usbip for info about devices on the Windows host
These utility commands can be ran from command line with either usbip on WSL or usbpid on Windows. Just note, the usage varies slightly, so check the docs. I like to run the commands from within WSL. First let’s get the host IP address from /etc/resolv.conf.
$ cat /etc/resolv.conf
# This file was automatically generated by WSL. To stop automatic generation of this file, add the following entry to /etc/wsl.conf:
# [network]
# generateResolvConf = false
nameserver 172.29.176.1
Grab the IP and use it with the remote flag for listing exportable USB devices on the host.
$ usbip list -r 172.29.176.1
Exportable USB devices
======================
- 172.29.176.1
1-11: Yubico.com : Yubikey 4 OTP+U2F+CCID (1050:0407)
: USB\VID_1050&PID_0407\6&20381000&0&11
: (Defined at Interface level) (00/00/00)
: 0 - Human Interface Device / Boot Interface Subclass / Keyboard (03/01/01)
: 1 - Human Interface Device / No Subclass / None (03/00/00)
: 2 - Chip/SmartCard / unknown subclass / unknown protocol (0b/00/00)
Installing YubiKey utilities and attaching the device
Let’s install the YubiKey manager command line tool from the package repository.
$ sudo apt install yubikey-manager
We can see what commands are available to us with ykman
Usage: ykman [OPTIONS] COMMAND [ARGS]...
Configure your YubiKey via the command line.
Examples:
List connected YubiKeys, only output serial number:
$ ykman list --serials
Show information about YubiKey with serial number 0123456:
$ ykman --device 0123456 info
Options:
-v, --version
-d, --device SERIAL
-l, --log-level [DEBUG|INFO|WARNING|ERROR|CRITICAL]
Enable logging at given verbosity level.
--log-file FILE Write logs to the given FILE instead of standard error; ignored unless --log-level
is also set.
-r, --reader NAME Use an external smart card reader. Conflicts with --device and list.
-h, --help Show this message and exit.
Commands:
config Enable/Disable applications.
fido Manage FIDO applications.
info Show general information.
list List connected YubiKeys.
mode Manage connection modes (USB Interfaces).
oath Manage OATH Application.
openpgp Manage OpenPGP Application.
otp Manage OTP Application.
piv Manage PIV Application.
Attach the device with usbip from Linux so we can use this utility to get more info about it. We can get both the IP for the remote host, as well as the bus id from the usbip list
command we ran above.
$ sudo usbip attach -r 172.29.176.1 --busid=1-11
If all goes well, we should be able to query the card now with ykman
$ ykman list
YubiKey 5 NFC [OTP+FIDO+CCID] Serial: XXXXXXXX
ykman info
Device type: YubiKey 5 NFC
Serial number: XXXXXXXX
Firmware version: 5.4.3
Form factor: Keychain (USB-A)
Enabled USB interfaces: OTP+FIDO+CCID
NFC interface is enabled.
Applications USB NFC
OTP Enabled Enabled
FIDO U2F Enabled Enabled
OpenPGP Enabled Enabled
PIV Enabled Enabled
OATH Enabled Enabled
FIDO2 Enabled Enabled
As one my main use cases for the YubiKey was to authenticate to GitHub and other services via SSH, I’m interested in using FIDO2 for storing resident keys. Let’s try to get some info about what keys I have already stored.
$ ykman fido list
Traceback (most recent call last):
File "/usr/bin/ykman", line 11, in <module>
load_entry_point('yubikey-manager==3.1.1', 'console_scripts', 'ykman')()
File "/usr/lib/python3/dist-packages/ykman/cli/__main__.py", line 273, in main
cli(obj={})
File "/usr/lib/python3/dist-packages/click/core.py", line 764, in __call__
return self.main(*args, **kwargs)
File "/usr/lib/python3/dist-packages/click/core.py", line 717, in main
rv = self.invoke(ctx)
File "/usr/lib/python3/dist-packages/click/core.py", line 1137, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "/usr/lib/python3/dist-packages/click/core.py", line 1137, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "/usr/lib/python3/dist-packages/click/core.py", line 956, in invoke
return ctx.invoke(self.callback, **ctx.params)
File "/usr/lib/python3/dist-packages/click/core.py", line 555, in invoke
return callback(*args, **kwargs)
File "/usr/lib/python3/dist-packages/click/decorators.py", line 17, in new_func
return f(get_current_context(), *args, **kwargs)
File "/usr/lib/python3/dist-packages/ykman/cli/fido.py", line 112, in list_creds
controller = ctx.obj['controller']
File "/usr/lib/python3/dist-packages/ykman/cli/util.py", line 127, in __getitem__
self.resolve()
File "/usr/lib/python3/dist-packages/ykman/cli/util.py", line 124, in resolve
self._objects[k] = f()
File "/usr/lib/python3/dist-packages/ykman/cli/__main__.py", line 194, in resolve_device
dev = _run_cmd_for_single(ctx, subcmd.name, transports, reader)
File "/usr/lib/python3/dist-packages/ykman/cli/__main__.py", line 132, in _run_cmd_for_single
return descriptor.open_device(transports)
File "/usr/lib/python3/dist-packages/ykman/descriptor.py", line 96, in open_device
for drv in _list_drivers(transports):
File "/usr/lib/python3/dist-packages/ykman/descriptor.py", line 164, in _list_drivers
for dev in open_fido():
File "/usr/lib/python3/dist-packages/ykman/driver_fido.py", line 97, in open_devices
for dev in CtapHidDevice.list_devices(descriptor_filter):
File "/usr/lib/python3/dist-packages/fido2/hid.py", line 135, in list_devices
for d in hidtransport.hid.Enumerate():
File "/usr/lib/python3/dist-packages/fido2/_pyu2f/linux.py", line 183, in Enumerate
for hidraw in os.listdir('/sys/class/hidraw'):
FileNotFoundError: [Errno 2] No such file or directory: '/sys/class/hidraw'
Exception: ykman exited with 1
[tty 4], line 1: ykman fido list
Uh oh, something’s not quite right…
But what does it all mean?
Looking at the traceback, we can see the ykman script fails due to not being able to find '/sys/class/hidraw'
. Because USB support in WSL is quite new, it seems not all of the HID drivers have been enabled yet in the kernel. So what does this mean for us?
Giving you the keys to the Lamborghini
As a budding Linux script kiddie, I was always awestruck reading accounts of people compiling their own kernels for system optimization and support for esoteric hardware. Back then it seemed like a task fit only for the most capable practitioners of the *nix dark arts.
Today, thanks to the wonderful folks working on open source projects at Microsoft and elsewhere, it is ridiculously simple to get a copy of their kernel source, modify and load it into WSL with no fear of bricking your whole system. Let’s do that now. First we need to install some build dependencies and pull in the kernel source.
$ sudo apt install build-essential flex bison libssl-dev libelf-dev
$ git clone https://github.com/microsoft/WSL2-Linux-Kernel.git
Once the source finishes downloading, cd
into the directory and create a copy of Microsoft’s build config to the root so we have a backup to revert to should things go awry.
$ cp Microsoft/config-wsl .config
We have a couple options available to us for modifying the config. You can edit the config manually in your text editor, I opted to use the graphical configuration editor included with make
, which in this case was a bit overkill because I only needed to change one line. For brevity, let’s do it the easy way. Open up the .config
file in an editor.
$ nano .config
Find the line that says
# CONFIG_HIDRAW is not set
And replace with
CONFIG_HIDRAW=y
Now it’s time to compile our kernel! Run make
and wait…
$ make
...
...
...
Kernel: arch/x86/boot/bzImage is ready (#1)
If you see the above message, give yourself a pat on the back for a job well done, you just compiled a Linux kernel! Let’s copy that image to our Windows filesystem so that we can tell WSL where to load it from.
$ cp arch/x86/boot/bzImage /mnt/c/Users/<your_windows_user>/custom-wsl-kernel
systemd
While it may not be strictly necessary, as Microsoft does have it’s own way of managing what services start up at boot, systemd has become the standard across most distros these days. I recommend this step, because this is what worked for me. Systemd support is also a new feature as of WSL2 version 0.67.6. You can check your version by running wsl --version
and wsl --update
if you are behind. Enabling systemd is as easy as creating or editing your /etc/wsl.conf
. Add these lines:
[boot]
systemd=true
One last thing
With that out of the way, we can shut down WSL again to take care of a few final things on Windows.
wsl --shutdown
The only thing left to do now is edit our .wslconfig
to tell the system where our kernel is. Open up C:\Users\<your_user>\.wslconfig
and add
[wsl2]
kernel=C:\\Users\<your_user>\custom-wsl-kernel
That’s it!
And now for the moment of truth…
Time to open up WSL and test it out. Let’s try running our ykman
utility again to check out what it knows about FIDO on our card.
# Attach the YubiKey again
$ sudo usbip attach -r 172.29.176.1 --busid=1-11
# Check what we get
$ ykman fido list
Enter your PIN:
ni@nunya.com (login.microsoft.com)
openssh (ssh:)
$ ykman fido info
PIN is set, with 8 tries left.
It works! Feel free to do a little victory dance. Now you can generate/add your own resident keys and use FIDO2 from your WSL guests.